﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Xml;

namespace ReferenceSwap
{
    // todo: would be handy if we could just "save" using just generated xml from the references
    // would let us just work with the Lists which would make the Add/Remove type of operations much simpler

    public class ProjectData : IDisposable // making this idisposable is ... eh...
    {
        private XmlDocument fullXml;
        private bool hasChanges;
        private bool ignoreSystemReferences;
        private List<string> systemReferences = new List<string>() { "System", "Microsoft.CSharp" };
        private List<FileReference> files;
        private List<ProjectReference> projectReferences;
        private List<AssemblyReference> assemblyReferences;

        public ProjectData(string projectFile) :
            this(projectFile, true) { }

        public ProjectData(string projectFile, bool ignoreSystem)
        {
            Guard.Against<ArgumentException>(string.IsNullOrEmpty(projectFile), "ProjectFile must be a valid value.");
            Guard.Against<FileNotFoundException>(!File.Exists(projectFile), string.Format("Unable to locate project file: {0}.", projectFile));

            ignoreSystemReferences = ignoreSystem;

            ProjectFile = new FileInfo(projectFile);

            fullXml = new XmlDocument();
            fullXml.Load(ProjectFile.FullName);

            Parse();
        }

        #region Properties
        /// <summary>
        /// Gets the Project File
        /// </summary>
        public FileInfo ProjectFile { get; private set; }
        /// <summary>
        /// Gets the Output description
        /// </summary>
        public string Output { get; private set; }
        /// <summary>
        /// Gets the output type
        /// </summary>
        public OutputType TypeOfOutput { get; private set; }
        /// <summary>
        /// Gets the output file
        /// </summary>
        public string OutputFile
        {
            get
            {
                return Output + (TypeOfOutput == OutputType.Library ? ".dll" : ".exe");
            }
        }
        /// <summary>
        /// Gets a collection of all references in the project
        /// </summary>
        public List<Reference> References
        {
            get
            {
                List<Reference> refs = new List<Reference>();
                // can't use add range in .net 2 if not same type
                foreach (ProjectReference r in ProjectReferences)
                    refs.Add((Reference)r);

                foreach (AssemblyReference r in AssemblyReferences)
                    refs.Add((Reference)r);

                // add file refs too?
                //foreach (FileReference r in Files)
                //    refs.Add((Reference)r);

                return refs;
            }
        }
        /// <summary>
        /// Gets the collection of all Project References
        /// </summary>
        public List<ProjectReference> ProjectReferences
        {
            get
            {
                if (projectReferences == null)
                    projectReferences = new List<ProjectReference>();

                return projectReferences;
            }
        }
        /// <summary>
        /// Gets the collection of all Assembly References
        /// </summary>
        public List<AssemblyReference> AssemblyReferences
        {
            get
            {
                if (assemblyReferences == null)
                    assemblyReferences = new List<AssemblyReference>();

                return assemblyReferences;
            }
        }
        /// <summary>
        /// Gets the collection of all Files
        /// </summary>
        public List<FileReference> Files
        {
            get
            {
                if (files == null)
                    files = new List<FileReference>();

                return files;
            }
        }

        #endregion
        /// <summary>
        /// Attempt to find the solution based on the project location
        /// </summary>
        /// <returns>auto-discovered solution</returns>
        /// <exception cref="FileNotFoundException"/>
        public SolutionData FindSolution()
        {
            // try the current directory
            FileInfo[] files = ProjectFile.Directory.GetFiles("*.sln");
            SolutionData sln = null;

            // if nothing - try one above - a default structure for visual studio
            if (files.Length == 0)
                files = ProjectFile.Directory.Parent.GetFiles("*.sln");

            // if we only find one - lets assume its the right one
            // will cause problems in some cases certainly
            if (files.Length == 1)
                sln = new SolutionData(files[0].FullName);
            else if (files.Length > 1)
            {
                foreach (FileInfo f in files)
                {
                    if (f.Name.Replace(f.Extension, "") == ProjectFile.Name.Replace(ProjectFile.Extension, ""))
                    {
                        sln = new SolutionData(f.FullName);
                        break;
                    }
                }
            }

            // maybe I should just return null and let the app side handle it?
            if (sln == null)
                throw new FileNotFoundException("Unable to auto-locate solution file");

            return sln;
        }
        /// <summary>
        /// Remove a project reference
        /// </summary>
        /// <param name="reference">reference to remove</param>
        public void RemoveProjectReference(ProjectReference reference)
        {
            XmlNodeList nodes = fullXml.GetElementsByTagName("ProjectReference");
            XmlElement nodeToRemove = null;
            foreach (XmlElement node in nodes)
            {
                if (node.Attributes["Include"].Value == reference.Path)
                {
                    nodeToRemove = node;
                    break;
                }
            }

            if (nodeToRemove != null)
            {
                nodeToRemove.ParentNode.RemoveChild(nodeToRemove);
                hasChanges = true;
            }
            else
                throw new Exception(string.Format("Unable to find project reference {0}.", reference.Name));

            Parse();
        }
        /// <summary>
        /// Change an Assembly Hint Path
        /// </summary>
        /// <param name="reference">reference to change</param>
        /// <param name="newPath">new hint path</param>
        public void ChangeAssemblyReferenceHintPath(AssemblyReference reference, string newPath)
        {
            XmlNodeList hintPaths = fullXml.GetElementsByTagName("HintPath");

            foreach (XmlElement hintPath in hintPaths)
            {
                if (hintPath.InnerText == reference.Path)
                {
                    hintPath.InnerText = newPath;
                    NotifyXmlChanges();
                    break;
                }
            }
        }
        /// <summary>
        /// Add an assembly reference
        /// </summary>
        /// <param name="reference">reference to add</param>
        /// <exception cref="InvalidDataException" />
        public void AddAssemblyReference(AssemblyReference reference)
        {
            XmlElement node = FindItemGroup("Reference");
            if (node == null)
                throw new InvalidDataException(string.Format("Unable to find an item group of children with tag {0}.", "Reference"));

            node.InnerXml += reference.GenerateXml();
            NotifyXmlChanges();
            return;
        }
        /// <summary>
        /// Update a file reference
        /// </summary>
        /// <param name="file">reference to update</param>
        public void UpdateFileReference(FileReference file)
        {
            XmlElement node = FindItemGroup(file.Type.ToString());

            if (node == null)
                throw new InvalidDataException(string.Format("Unable to find an item group of children with tag {0}.", file.Type));

            for (int i = 0; i < node.ChildNodes.Count; i++)
            {
                XmlElement child = (XmlElement)node.ChildNodes[i];
                var current = FileReference.LoadFromXml(child);
                if (current.Name == file.Name)
                {
                    node.RemoveChild(child);
                    node.InnerXml += file.GenerateXml();
                    NotifyXmlChanges();
                    return;
                }

            }
        }

        /// <summary>
        /// Save any changes
        /// </summary>
        public void Save()
        {
            if (!hasChanges) return;

            XmlWriterSettings settings = new XmlWriterSettings();
            settings.Indent = true;
            using (XmlWriter writer = XmlWriter.Create(ProjectFile.FullName, settings))
            {
                fullXml.WriteTo(writer);
            }
        }

        #region Helpers

        private void NotifyXmlChanges()
        {
            hasChanges = true;
            Parse();
        }

        private XmlElement FindItemGroup(string childType)
        {
            XmlNodeList nodes = fullXml.GetElementsByTagName("ItemGroup");
            foreach (XmlElement node in nodes)
            {
                if (node.HasChildNodes && node.FirstChild.Name == childType)
                    return node;
            }

            return null;
        }

        private void Parse()
        {
            Guard.Against<InvalidDataException>(fullXml == null, "The Xml for the project file was not properly loaded.");

            // this is pretty of ugly - maybe each class should be able to parse their own xml information?

            ProjectReferences.Clear();
            AssemblyReferences.Clear();

            XmlNodeList nodes = fullXml.GetElementsByTagName("AssemblyName");
            Output = nodes[0].InnerText;

            nodes = fullXml.GetElementsByTagName("OutputType");
            TypeOfOutput = ParseOutputType(nodes[0].InnerText);

            nodes = fullXml.GetElementsByTagName("Reference");
            foreach (XmlElement node in nodes)
            {
                string include = node.Attributes["Include"].Value;

                if (IgnoreReference(include)) // ignore the standard system references
                    continue;

                AssemblyReference r = new AssemblyReference();
                r.ParseFullName(include);
                foreach (XmlElement child in node.ChildNodes)
                {
                    if (child.Name == "SpecificVersion")
                        r.SpecificVersion = bool.Parse(child.InnerText);
                    else if (child.Name == "HintPath")
                        r.Path = child.InnerText;
                }
                AssemblyReferences.Add(r);
            }

            nodes = fullXml.GetElementsByTagName("ProjectReference");
            foreach (XmlElement node in nodes)
            {
                ProjectReference r = new ProjectReference();
                r.Path = node.Attributes["Include"].Value;

                foreach (XmlElement child in node.ChildNodes)
                {
                    if (child.Name == "Project")
                        r.Guid = child.InnerText;
                    else if (child.Name == "Name")
                        r.Name = child.InnerText;
                }

                ProjectReferences.Add(r);
            }

            ParseContentFiles(fullXml.GetElementsByTagName("Content"));
            // should we even parse nones?
            ParseContentFiles(fullXml.GetElementsByTagName("None"));
            // todo: parse compile files so we can do a sync

            // anything else to parse?
        }

        private void ParseContentFiles(XmlNodeList nodes)
        {
            foreach (XmlElement node in nodes)
            {
                Files.Add(FileReference.LoadFromXml(node));
            }
        }

        private bool IgnoreReference(string includeValue)
        {
            if (!ignoreSystemReferences)
                return false;

            foreach (var sysRef in systemReferences)
            {
                if (includeValue.StartsWith(sysRef))
                    return true;
            }

            return false;
        }

        private OutputType ParseOutputType(string typeText)
        {
            switch (typeText)
            {
                case "WinExe":
                case "Exe":
                    return ReferenceSwap.OutputType.Executable;
                case "Library":
                default:
                    return ReferenceSwap.OutputType.Library;
            }
        }

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            if (hasChanges)
            {
                Save();
            }
        }

        #endregion
    }
}
